package in.omerjerk.remotedroid.app;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.WindowManager;
import android.widget.Toast;
import com.koushikdutta.async.ByteBufferList;
import com.koushikdutta.async.DataEmitter;
import com.koushikdutta.async.callback.CompletedCallback;
import com.koushikdutta.async.callback.DataCallback;
import com.koushikdutta.async.http.WebSocket;
import com.koushikdutta.async.http.server.AsyncHttpServer;
import com.koushikdutta.async.http.server.AsyncHttpServerRequest;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import in.umairkhan.remotedroid.R;
public class ServerService extends Service {
private MediaCodec encoder = null;
private static final String TAG = "omerjerk";
private int serverPort;
private float bitrateRatio;
private AsyncHttpServer server;
private List<WebSocket> _sockets = new ArrayList<WebSocket>();
Thread encoderThread = null;
Handler mHandler;
SharedPreferences preferences;
static int deviceWidth;
static int deviceHeight;
Point resolution = new Point();
private static boolean LOCAL_DEBUG = false;
VideoWindow videoWindow = null;
private VirtualDisplay virtualDisplay;
private class ToastRunnable implements Runnable {
String mText;
public ToastRunnable(String text) {
mText = text;
}
@Override
public void run() {
Toast.makeText(getApplicationContext(), mText, Toast.LENGTH_SHORT).show();
}
}
/**
* Main Entry Point of the server code.
* Create a WebSocket server and start the encoder.
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && intent.getAction() == "STOP") {
dispose();
return START_NOT_STICKY;
}
if (server == null && intent.getAction().equals("START")) {
preferences = PreferenceManager.getDefaultSharedPreferences(this);
LOCAL_DEBUG = preferences.getBoolean("local_debugging", false);
DisplayMetrics dm = new DisplayMetrics();
Display mDisplay = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
mDisplay.getMetrics(dm);
deviceWidth = dm.widthPixels;
deviceHeight = dm.heightPixels;
float resolutionRatio = Float.parseFloat(
preferences.getString(SettingsActivity.KEY_RESOLUTION_PREF, "0.25"));
mDisplay.getRealSize(resolution);
resolution.x = (int) (resolution.x * resolutionRatio);
resolution.y = (int) (resolution.y * resolutionRatio);
if (!LOCAL_DEBUG) {
server = new AsyncHttpServer();
server.websocket("/", null, websocketCallback);
serverPort = Integer.parseInt(preferences.getString(SettingsActivity.KEY_PORT_PREF, "6060"));
bitrateRatio = Float.parseFloat(preferences.getString(SettingsActivity.KEY_BITRATE_PREF, "1"));
updateNotification("Streaming is live at");
server.listen(serverPort);
new Thread(new Runnable() {
@Override
public void run() {
showToast("Starting main touch server");
new MainStarter(ServerService.this).start();
showToast("started main touch server");
}
}).start();
} else {
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
params.gravity = Gravity.TOP | Gravity.LEFT;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
videoWindow = (VideoWindow) inflater.inflate(R.layout.video_window, null);
windowManager.addView(videoWindow, params);
videoWindow.inflateSurfaceView();
if (encoderThread == null) {
encoderThread = new Thread(new EncoderWorker(), "Encoder Thread");
encoderThread.start();
}
}
mHandler = new Handler();
}
return START_NOT_STICKY;
}
private AsyncHttpServer.WebSocketRequestCallback websocketCallback = new AsyncHttpServer.WebSocketRequestCallback() {
@Override
public void onConnected(final WebSocket webSocket, AsyncHttpServerRequest request) {
_sockets.add(webSocket);
showToast("Someone just connected");
//Start rendering display on the surface and setting up the encoder
if (encoderThread == null) {
startDisplayManager();
encoderThread = new Thread(new EncoderWorker(), "Encoder Thread");
encoderThread.start();
}
//Use this to clean up any references to the websocket
webSocket.setClosedCallback(new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
try {
if (ex != null)
ex.printStackTrace();
} finally {
_sockets.clear();
}
showToast("Disconnected");
dispose();
}
});
webSocket.setStringCallback(new WebSocket.StringCallback() {
@Override
public void onStringAvailable(String s) {
Log.d(TAG, "String received. No idea what to do with it.");
}
});
webSocket.setDataCallback(new DataCallback() {
@Override
public void onDataAvailable(DataEmitter dataEmitter, ByteBufferList byteBufferList) {
byteBufferList.recycle();
}
});
}
};
/**
* Create the display surface out of the encoder. The data to encoder will be fed from this
* Surface itself.
* @return
* @throws IOException
*/
@TargetApi(19)
private Surface createDisplaySurface() throws IOException {
MediaFormat mMediaFormat = MediaFormat.createVideoFormat(CodecUtils.MIME_TYPE,
CodecUtils.WIDTH, CodecUtils.HEIGHT);
mMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, (int) (1024 * 1024 * 0.5));
mMediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
mMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
mMediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
Log.i(TAG, "Starting encoder");
encoder = MediaCodec.createEncoderByType(CodecUtils.MIME_TYPE);
encoder.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface surface = encoder.createInputSurface();
return surface;
}
@TargetApi(19)
public void startDisplayManager() {
DisplayManager mDisplayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
Surface encoderInputSurface = null;
try {
encoderInputSurface = createDisplaySurface();
} catch (IOException e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
virtualDisplay = mDisplayManager.createVirtualDisplay("Remote Droid", CodecUtils.WIDTH, CodecUtils.HEIGHT, 50,
encoderInputSurface,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC | DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE);
} else {
if (MainActivity.mMediaProjection != null) {
virtualDisplay = MainActivity.mMediaProjection.createVirtualDisplay("Remote Droid",
CodecUtils.WIDTH, CodecUtils.HEIGHT, 50,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
encoderInputSurface, null, null);
} else {
showToast("Something went wrong. Please restart the app.");
}
}
encoder.start();
}
@TargetApi(19)
private class EncoderWorker implements Runnable {
@Override
public void run() {
startDisplayManager();
ByteBuffer[] encoderOutputBuffers = encoder.getOutputBuffers();
boolean encoderDone = false;
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
String infoString;
while (!encoderDone) {
int encoderStatus;
try {
encoderStatus = encoder.dequeueOutputBuffer(info, CodecUtils.TIMEOUT_USEC);
} catch (IllegalStateException e) {
e.printStackTrace();
break;
}
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet
//Log.d(TAG, "no output from encoder available");
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// not expected for an encoder
encoderOutputBuffers = encoder.getOutputBuffers();
Log.d(TAG, "encoder output buffers changed");
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// not expected for an encoder
MediaFormat newFormat = encoder.getOutputFormat();
Log.d(TAG, "encoder output format changed: " + newFormat);
} else if (encoderStatus < 0) {
break;
} else {
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
if (encodedData == null) {
Log.d(TAG, "============It's NULL. BREAK!=============");
return;
}
if (!LOCAL_DEBUG) {
for (WebSocket socket : _sockets) {
infoString = info.offset + "," + info.size + "," +
info.presentationTimeUs + "," + info.flags;
socket.send(infoString.getBytes());
byte[] b = new byte[info.size];
try {
if (info.size != 0) {
encodedData.limit(info.offset + info.size);
encodedData.position(info.offset);
encodedData.get(b, info.offset, info.offset + info.size);
socket.send(b);
}
} catch (BufferUnderflowException e) {
e.printStackTrace();
}
}
} else {
if (info.size != 0) {
encodedData.position(info.offset);
encodedData.limit(info.offset + info.size);
}
videoWindow.setData(CodecUtils.clone(encodedData), info);
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
Log.w(TAG, "config flag received");
}
}
encoderDone = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
try {
encoder.releaseOutputBuffer(encoderStatus, false);
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
dispose();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void showToast(final String message) {
mHandler.post(new ToastRunnable(message));
}
/**
* Display the notification
* @param message
*/
private void updateNotification(String message) {
Intent intent = new Intent(this, ServerService.class);
intent.setAction("STOP");
PendingIntent stopServiceIntent = PendingIntent.getService(this, 0, intent, 0);
Notification.Builder mBuilder =
new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_launcher)
.setOngoing(true)
.addAction(R.drawable.ic_media_stop, "Stop", stopServiceIntent)
.setContentTitle(message)
.setContentText(Utils.getIPAddress(true) + ":" + serverPort);
startForeground(6000, mBuilder.build());
}
private void dispose() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (virtualDisplay != null)
virtualDisplay.release();
}
if (encoder != null) {
encoder.signalEndOfInputStream();
encoder.stop();
encoder.release();
encoder = null;
}
if (server != null) {
server.stop();
server = null;
}
stopForeground(true);
stopSelf();
}
}